/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Contributors:   Mathias Bogaert
package org.apache.log4j.xml;

import org.apache.log4j.Layout;
import org.apache.log4j.helpers.Transform;
import org.apache.log4j.spi.LocationInfo;
import org.apache.log4j.spi.LoggingEvent;

import java.util.Set;
import java.util.Arrays;

/**
 * The output of the XMLLayout consists of a series of log4j:event elements as
 * defined in the <a href="log4j.dtd">log4j.dtd</a>. It does not output a
 * complete well-formed XML file. The output is designed to be included as an
 * <em>external entity</em> in a separate file to form a correct XML file.
 * 
 * <p>
 * For example, if <code>abc</code> is the name of the file where the XMLLayout
 * ouput goes, then a well-formed XML file would be:
 * 
 * <pre>
 * &lt;?xml version=&quot;1.0&quot; ?&gt;
 *  
 *   &lt;!DOCTYPE log4j:eventSet SYSTEM &quot;log4j.dtd&quot; [&lt;!ENTITY data SYSTEM &quot;abc&quot;&gt;]&gt;
 *  
 *   &lt;log4j:eventSet version=&quot;1.2&quot; xmlns:log4j=&quot;http://jakarta.apache.org/log4j/&quot;&gt;
 *  	  &amp;data
 *   &lt;/log4j:eventSet&gt;
 * </pre>
 * 
 * <p>
 * This approach enforces the independence of the XMLLayout and the appender
 * where it is embedded.
 * 
 * <p>
 * The <code>version</code> attribute helps components to correctly intrepret
 * output generated by XMLLayout. The value of this attribute should be "1.1"
 * for output generated by log4j versions prior to log4j 1.2 (final release) and
 * "1.2" for relase 1.2 and later.
 * 
 * Appenders using this layout should have their encoding set to UTF-8 or
 * UTF-16, otherwise events containing non ASCII characters could result in
 * corrupted log files.
 * 
 * @author Ceki G&uuml;lc&uuml;
 * @since 0.9.0
 * */
public class XMLLayout extends Layout {

	private final int DEFAULT_SIZE = 256;
	private final int UPPER_LIMIT = 2048;

	private StringBuffer buf = new StringBuffer(DEFAULT_SIZE);
	private boolean locationInfo = false;
	private boolean properties = false;

	/**
	 * The <b>LocationInfo</b> option takes a boolean value. By default, it is
	 * set to false which means there will be no location information output by
	 * this layout. If the the option is set to true, then the file name and
	 * line number of the statement at the origin of the log statement will be
	 * output.
	 * 
	 * <p>
	 * If you are embedding this layout within an
	 * {@link org.apache.log4j.net.SMTPAppender} then make sure to set the
	 * <b>LocationInfo</b> option of that appender as well.
	 * */
	public void setLocationInfo(boolean flag) {
		locationInfo = flag;
	}

	/**
	 * Returns the current value of the <b>LocationInfo</b> option.
	 */
	public boolean getLocationInfo() {
		return locationInfo;
	}

	/**
	 * Sets whether MDC key-value pairs should be output, default false.
	 * 
	 * @param flag
	 *            new value.
	 */
	public void setProperties(final boolean flag) {
		properties = flag;
	}

	/**
	 * Gets whether MDC key-value pairs should be output.
	 * 
	 * @return true if MDC key-value pairs are output.
	 */
	public boolean getProperties() {
		return properties;
	}

	/** No options to activate. */
	public void activateOptions() {
	}

	/**
	 * Formats a {@link org.apache.log4j.spi.LoggingEvent} in conformance with
	 * the log4j.dtd.
	 * */
	public String format(final LoggingEvent event) {

		// Reset working buffer. If the buffer is too large, then we need a new
		// one in order to avoid the penalty of creating a large array.
		if (buf.capacity() > UPPER_LIMIT) {
			buf = new StringBuffer(DEFAULT_SIZE);
		} else {
			buf.setLength(0);
		}

		// We yield to the \r\n heresy.

		buf.append("<log4j:event logger=\"");
		buf.append(Transform.escapeTags(event.getLoggerName()));
		buf.append("\" timestamp=\"");
		buf.append(event.timeStamp);
		buf.append("\" level=\"");
		buf.append(Transform.escapeTags(String.valueOf(event.getLevel())));
		buf.append("\" thread=\"");
		buf.append(Transform.escapeTags(event.getThreadName()));
		buf.append("\">\r\n");

		buf.append("<log4j:message><![CDATA[");
		// Append the rendered message. Also make sure to escape any
		// existing CDATA sections.
		Transform.appendEscapingCDATA(buf, event.getRenderedMessage());
		buf.append("]]></log4j:message>\r\n");

		String ndc = event.getNDC();
		if (ndc != null) {
			buf.append("<log4j:NDC><![CDATA[");
			Transform.appendEscapingCDATA(buf, ndc);
			buf.append("]]></log4j:NDC>\r\n");
		}

		String[] s = event.getThrowableStrRep();
		if (s != null) {
			buf.append("<log4j:throwable><![CDATA[");
			for (int i = 0; i < s.length; i++) {
				Transform.appendEscapingCDATA(buf, s[i]);
				buf.append("\r\n");
			}
			buf.append("]]></log4j:throwable>\r\n");
		}

		if (locationInfo) {
			LocationInfo locationInfo = event.getLocationInformation();
			buf.append("<log4j:locationInfo class=\"");
			buf.append(Transform.escapeTags(locationInfo.getClassName()));
			buf.append("\" method=\"");
			buf.append(Transform.escapeTags(locationInfo.getMethodName()));
			buf.append("\" file=\"");
			buf.append(Transform.escapeTags(locationInfo.getFileName()));
			buf.append("\" line=\"");
			buf.append(locationInfo.getLineNumber());
			buf.append("\"/>\r\n");
		}

		if (properties) {
			Set keySet = event.getPropertyKeySet();
			if (keySet.size() > 0) {
				buf.append("<log4j:properties>\r\n");
				Object[] keys = keySet.toArray();
				Arrays.sort(keys);
				for (int i = 0; i < keys.length; i++) {
					String key = keys[i].toString();
					Object val = event.getMDC(key);
					if (val != null) {
						buf.append("<log4j:data name=\"");
						buf.append(Transform.escapeTags(key));
						buf.append("\" value=\"");
						buf.append(Transform.escapeTags(String.valueOf(val)));
						buf.append("\"/>\r\n");
					}
				}
				buf.append("</log4j:properties>\r\n");
			}
		}

		buf.append("</log4j:event>\r\n\r\n");

		return buf.toString();
	}

	/**
	 * The XMLLayout prints and does not ignore exceptions. Hence the return
	 * value <code>false</code>.
	 */
	public boolean ignoresThrowable() {
		return false;
	}
}
